package fr.aendawyn.as3audioplayer {
	
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.IOErrorEvent;
	import flash.events.TimerEvent;
	import flash.media.ID3Info;
	import flash.media.Sound;
	import flash.media.SoundChannel;
	import flash.media.SoundTransform;
	import flash.net.URLRequest;
	import flash.utils.Timer;
	import fr.aendawyn.as3audioplayer.errors.AudioPlayerError;
	import fr.aendawyn.as3audioplayer.events.AudioPlayerEvent;
	import fr.aendawyn.as3audioplayer.events.specialize.EndPlayingEvent;
	import fr.aendawyn.as3audioplayer.events.specialize.ProgressionEvent;
	import fr.aendawyn.as3audioplayer.events.specialize.StartPlayingEvent;
	import fr.aendawyn.as3audioplayer.events.specialize.TrackInfosEvent;
	
	[Event(name = "start_playing",
		type = "fr.aendawyn.as3lecteuraudio.events.specialise.StartPlayingEvent")]
	[Event(name = "end_playing",
		type = "fr.aendawyn.as3lecteuraudio.events.specialise.EndPlayingEvent")]
	[Event(name = "progress",
		type = "fr.aendawyn.as3lecteuraudio.events.specialise.ProgressionEvent")]
	[Event(name = "available_infos",
		type = "fr.aendawyn.as3lecteuraudio.events.specialise.TrackInfosEvent")]
	/**	<p>La classe <code>LecteurAudio</code> permet de créer facilement un lecteur audio en AS3.</p>
	 * 	<p>Le lecteur audio gere les liste de lecture, la lecture aleatoire et repetee, la reprise
	 * 	de lecture, le changement de volume, ect...</p>
	 *
	 * @author Adrien Quillet
	 */
	public class AudioPlayer extends EventDispatcher {
		private var _randomPlayOrder:Vector.<String>;
		private var _playList:Vector.<String>;
		private var _currentTrack:Track;
		private var _sound:Sound;
		private var _soundChannel:SoundChannel;
		private var _soundProperties:SoundTransform;
		private var _currentTrackPosition:uint;
		private var _currentTrackNumber:int;
		private var _repeatPlayList:Boolean;
		private var _randomPlayList:Boolean;
		private var _isPlaying:Boolean;
		private var _isPausing:Boolean;
		private var _currentPositionTimer:Timer;
		private var _debugLevel:int;
		
		/**	Cree une instance de <code>LecteurAudio</code>.
		 *
		 */
		public function AudioPlayer() {
			_playList = new Vector.<String>;
			_randomPlayOrder = new Vector.<String>;
			_soundProperties = new SoundTransform;
			_soundProperties.volume = 1;
			_soundProperties.pan = 0;
			_randomPlayList = false;
			_repeatPlayList = true;
			_isPlaying = false;
			_isPausing = false;
			_currentTrackNumber = -1;
			_debugLevel = 0;
			_currentPositionTimer = new Timer(100);
			_currentPositionTimer.addEventListener(TimerEvent.TIMER, handlerProgressEvent);
		}
		
		/*	----------------------------------------------------------- */
		/*	---------------		Gestion des pistes		--------------- */
		/*	----------------------------------------------------------- */
		/**	<p>Permet d'ajouter une piste à la liste de lecture courante.</p>
		 *
		 * 	@param urlPiste		l'url vers la piste à ajouter. Il peut être absolu ou relatif.
		 */
		public function addTrack(trackUrl:String):void {
			log(1, "---------------\nAdding new track");
			_playList.push(trackUrl);
			log(1, "Added track : " + trackUrl);
			addTrackRandomPlayList(trackUrl);
		}
		
		/**	<p>Permet le retrait d'une piste à la liste de lecture courante.</p>
		 *
		 * 	@param identitePiste
		 *	<p>Peut être de deux types différents :
		 * 	<ul>
		 * 		<li><b>int</b> : la piste du même numéro est alors retirée
		 * 			de la liste de lecture courante</li>
		 * 		<li><b>String</b> : la piste possedant l'url spécifié est alors
		 * 			retirée de la liste de lecture courante</li>
		 * 		</ul>
		 * 	</p>
		 * 	@throws ErreurLecteurAudio si le type de <code>identitepiste</code> est autre que
		 * 	<code>int</code> ou <code>String</code>.
		 */
		public function removeTrack(trackInfo:*):void {
			log(1, "---------------\nRemoving track");
			if (trackInfo is String) {
				log(1, "Removing track by URL");
				log(1, "URL to remove : " + trackInfo);
				removeTrackUrl(trackInfo);
			} else if (trackInfo is int || trackInfo is Number || trackInfo is uint) {
				log(1, "Removing track by number");
				log(1, "Track number :" + trackInfo);
				removeTrackNumber(trackInfo);
			} else {
				log(2, "Unable to remove track");
				throw new AudioPlayerError("Data format is not String or Number");
			}
		}
		
		private function removeTrackUrl(trackUrl:String):void {
			var newPlayList:Vector.<String> = new Vector.<String>;
			var url:String;
			var escapedTrackUrl:String = escape(trackUrl);
			if (_sound != null && _sound.url == escape(escapedTrackUrl)) {
				log(1, "Track is currently playing");
				log(1, "stop() will be invoked");
				stop();
			}
			for each (url in _playList) {
				if (escapedTrackUrl.length - escapedTrackUrl.search(escape(url)) != escape(url).length)
					newPlayList.push(url);
			}
			_playList = newPlayList;
			var newRandomPlayOrder:Vector.<String> = new Vector.<String>;
			for each (url in _randomPlayOrder) {
				if (escapedTrackUrl.length - escapedTrackUrl.search(escape(url)) != escape(url).length)
					newRandomPlayOrder.push(url);
			}
			_randomPlayOrder = newRandomPlayOrder;
		}
		
		private function removeTrackNumber(trackNumber:int):void {
			var playListLength:int = _playList.length;
			var newPlayList:Vector.<String> = new Vector.<String>;
			var i:int;
			var realTrackNumber:int = trackNumber - 1;
			if (_sound != null &&
				((_randomPlayList &&
				_playList[realTrackNumber] == _randomPlayOrder[_currentTrackNumber]) ||
				(!_randomPlayList && _sound.url == _playList[realTrackNumber]))) {
				log(1, "Track is currently playing");
				log(1, "stop() will be invoked");
				stop();
			}
			for (i = 0; i < playListLength; i++)
				if (realTrackNumber != i)
					newPlayList.push(_playList[i]);
			_playList = newPlayList;
			var newRandomPlayOrder:Vector.<String> = new Vector.<String>;
			for (i = 0; i < playListLength; i++)
				if (realTrackNumber != i)
					newRandomPlayOrder.push(_randomPlayOrder[i]);
			_randomPlayOrder = newRandomPlayOrder;
		}
		
		private function addTrackRandomPlayList(trackUrl:String):void {
			var randomInt:int = Math.floor(Math.random() * _playList.length);
			_randomPlayOrder.push(trackUrl);
			var tmp:String = _randomPlayOrder[randomInt];
			_randomPlayOrder[randomInt] = _randomPlayOrder[_randomPlayOrder.length - 1];
			_randomPlayOrder[_randomPlayOrder.length - 1] = tmp;
		}
		
		/*	----------------------------------------------------------- */
		/*	---------------		Contrôle de lecture		--------------- */
		/*	----------------------------------------------------------- */
		/**	<p>Lance la lecture de la liste de lecture courante.</p>
		 *
		 * 	@param numeroPiste
		 * 	Si différent de -1, lance la lecture de la piste correspondant au numéro spécifié
		 * 	en paramètre.
		 * 	@throws ErreurLecteurAudio si <code>numeroPiste</code> est spécifié et invalide (plus petit
		 * 	ou égal à 0, plus grand que le nombre de pistes dans la liste de lecture)
		 */
		public function play(trackNumber:int = -1):void {
			log(1, "---------------\nPlay");
			if (trackNumber != -1) {
				log(1, "Track number is specified");
				if (trackNumber < 1 || trackNumber > _playList.length) {
					log(2, "Track number is invalid");
					log(2, "Valid tracks numbers in [1;" + _playList.length + "]");
					throw new AudioPlayerError("Track number is invalid.");
				}
				log(1, "Asked track number : " + trackNumber);
				var trackUrl:String = _playList[trackNumber - 1];
				log(1, "Matched URL : " + trackUrl);
				if (_isPlaying)
					finalizeTrackPlaying();
				playTrack(trackUrl);
			} else {
				if (_isPlaying && _isPausing) {
					log(1, "Resume playback");
					_isPausing = false;
					_soundChannel = _sound.play(_currentTrackPosition, 0, _soundProperties);
					log(1, "Track : " + _sound.url);
					_soundChannel.addEventListener(Event.SOUND_COMPLETE, handlerTrackEnding);
				} else if (!_isPlaying) {
					log(1, "Playing new track");
					if (_playList.length != 0) {
						var newTrackUrl:String;
						var oldTrackNumber:int = _currentTrackNumber;
						do {
							_currentTrackNumber = (_currentTrackNumber + 1) % _playList.length;
						} while (_currentTrackNumber == _playList.length - 2 &&
							oldTrackNumber == _playList.length - 2);
						if (_currentTrackNumber < 0)
							_currentTrackNumber += _playList.length;
						if (_randomPlayList) {
							newTrackUrl = _randomPlayOrder[_currentTrackNumber];
						} else {
							newTrackUrl = _playList[_currentTrackNumber];
						}
						log(1, "Asked track URL : " + newTrackUrl);
						playTrack(newTrackUrl);
					}
				}
			}
		}
		
		private function playTrack(url:String):void {
			_sound = new Sound;
			_sound.addEventListener(IOErrorEvent.IO_ERROR, handlerIOError);
			_sound.addEventListener(Event.ID3, handlerID3);
			_sound.addEventListener(Event.COMPLETE, handlerLoadComplete);
			_currentTrack = new Track(giveFileName(url));
			_sound.load(new URLRequest(url));
		}
		
		private function finalizeTrackPlaying():void {
			try {
				log(1, "Finalizing track and sound channel");
				_currentPositionTimer.stop();
				_soundChannel.stop();
				_soundChannel = null;
				/*	Si le fichier audio était sur le disque local, _son.close()
				provoque l'erreur 2029. On catch les erreurs provenant de
				la fermeture du flex */
				_sound.close();
			} catch (e:Error) {
				log(2, "Catching error 2029");
				log(2, e);
				log(2, "Finalized");
			}
			_sound = null;
		}
		
		/**	<p>Met en pause la lecture courante. Un appel à <code>lecture()</code> permet la
		 * 	reprise.</p>
		 *
		 */
		public function pause():void {
			if (_isPlaying) {
				log(1, "---------------\nPause");
				_isPausing = true;
				_currentTrackPosition = _soundChannel.position;
				log(1, "Current track position : " + _currentTrackPosition);
				_soundChannel.stop();
			}
		}
		
		/**	<p>Arrête la lecture courante. Un appel à <code>lecture()</code> relance la piste
		 * 	qui a été arrêtée.</p>
		 *
		 */
		public function stop():void {
			if (_isPlaying) {
				log(1, "---------------\nStop");
				finalizeTrackPlaying();
				_isPlaying = false;
				_currentTrackPosition = 0;
				_currentTrackNumber--;
				dispatchEvent(new ProgressionEvent(0, 0));
			}
		}
		
		/**	<p>Stoppe la lecture courante et passe à la prochaine piste dans la liste de lecture.</p>
		 * 	<p>Si la fin de la liste de lecture est atteinte et que <code>lectureRepetee</code> est
		 * 	à <code>true</code>, la lecture reprend au début de la liste de lecture.</p>
		 * 	<p>S'il n'y avait pas de lecture en cours, la lecture se lance.</p>
		 *
		 */
		public function next():void {
			log(1, "---------------\nNext");
			_isPlaying = false;
			_isPausing = false;
			finalizeTrackPlaying();
			play();
		}
		
		/**	<p>Stoppe la lecture courante et passe à la prochaine pistepiste précédente dans la
		 * 	liste de lecture.</p>
		 * 	<p>Si la fin de la liste de lecture est atteinte et que <code>lectureRepetee</code> est
		 * 	à <code>true</code>, la lecture reprend au début de la liste de lecture.</p>
		 * 	<p>S'il n'y avait pas de lecture en cours, la lecture se lance.</p>
		 */
		public function previous():void {
			log(1, "---------------\nPrevious");
			_isPlaying = false;
			_isPausing = false;
			finalizeTrackPlaying();
			var trackUrl:String;
			if (_playList.length != 0) {
				_currentTrackNumber = (_currentTrackNumber - 1) % _playList.length;
				if (_currentTrackNumber < 0)
					_currentTrackNumber += _playList.length;
				if (_randomPlayList) {
					trackUrl = _randomPlayOrder[_currentTrackNumber];
				} else {
					trackUrl = _playList[_currentTrackNumber];
				}
				playTrack(trackUrl);
			}
		}
		
		/*	----------------------------------------------------------- */
		/*	---------------			Handlers			--------------- */
		/*	----------------------------------------------------------- */
		/**	<p>Se déclenche quand un fichier son est complètement chargé en mémoire.</p>
		 *
		 * 	@param e
		 * 	type Event.COMPLETE
		 */
		protected function handlerLoadComplete(e:Event):void {
			log(1, "---------------\nLoading file complete");
			log(1, "Loaded file :" + _sound.url);
			_sound.removeEventListener(Event.COMPLETE, handlerLoadComplete);
			_isPlaying = true;
			_soundChannel = _sound.play(0, 0, _soundProperties);
			dispatchEvent(new StartPlayingEvent(unescape(_sound.url),
														 _playList.indexOf(_sound.url) + 1));
			_soundChannel.addEventListener(Event.SOUND_COMPLETE, handlerTrackEnding);
			_currentPositionTimer.start();
			dispatchEvent(new TrackInfosEvent(_currentTrack));
		}
		
		/**	<p>Se déclenche lors d'une erreur d'entrée/sortie.</p>
		 *
		 * 	@param e
		 * 	type IOErrorEvent.IO_ERROR
		 * 	@throws ErreurLecteurAudio retransmet l'erreur captée
		 */
		protected function handlerIOError(e:IOErrorEvent):void {
			log(2, "---------------\nI/O Error");
			log(2, e);
			throw new AudioPlayerError(e.text);
		}
		
		/**	<p>Se déclenche à la reception des informations ID3 d'une piste audio.</p>
		 *
		 * 	@param e
		 * 	type Event.ID3
		 */
		protected function handlerID3(e:Event):void {
			log(1, "---------------\nID3 informations");
			_sound.removeEventListener(Event.ID3, handlerID3);
			var titre:String;
			if (_sound.id3.songName == "" || _sound.id3.songName == null) {
				log(2, "No information about track title");
				log(2, "Track title will be is filename");
				titre = currentTrack.title
			} else {
				titre = _sound.id3.songName;
			}
			_currentTrack =
				new Track(titre, _sound.id3.artist, int(_sound.id3.track), _sound.id3.album,
						  int(_sound.id3.year), _sound.id3.genre, _sound.id3.comment);
			log(1, "ID3 summary : " + _currentTrack.title + ' - ' + _currentTrack.artist);
			dispatchEvent(new TrackInfosEvent(_currentTrack));
		}
		
		/**	<p>Se déclenche quand la lecture d'une piste audio est terminée.</p>
		 *
		 * 	@param e
		 * 	type Event.SOUND_COMPLETE
		 */
		protected function handlerTrackEnding(e:Event):void {
			log(1, "---------------\nEnd playback");
			_sound.removeEventListener(IOErrorEvent.IO_ERROR, handlerIOError);
			_soundChannel.removeEventListener(Event.SOUND_COMPLETE, handlerTrackEnding);
			dispatchEvent(new EndPlayingEvent(unescape(_sound.url)));
			finalizeTrackPlaying();
			_isPlaying = false;
			_isPausing = false;
			if (_currentTrackNumber != _playList.length - 1 || _repeatPlayList) {
				log(1, "Next track");
				play();
			}
		}
		
		/**	<p>Permet de dispatcher la progression de la lecture courante</p>
		 *
		 * 	@param e
		 * 	type TimerEvent.TIMER
		 */
		protected function handlerProgressEvent(e:TimerEvent):void {
			dispatchEvent(new ProgressionEvent(_soundChannel.position, _sound.length));
		}
		
		/*	----------------------------------------------------------- */
		/*	---------------			Getters				--------------- */
		/*	----------------------------------------------------------- */
		[Bindable]
		/**	<p>Volume général du lecteur audio.</p>
		 * 	<p>Le volume est compris entre <code>0</code> (muet) et <code>100</code>
		 *  (maximum).</p>
		 *
		 * @return un <code>int</code> compris entre <code>0</code> et <code>100</code>
		 */
		public function get volume():int {
			return Math.round(_soundProperties.volume * 100);
		}
		
		[Bindable]
		/**	<p>Balance générale du lecteur audio. La balance permet d'équilibrer
		 * 	les voies droite/gauche du signal audio.</p>
		 *	<p>La balance est comprise entre <code>-100</code> (tout à gauche) et <code>100</code>
		 * 	(tout à droite). La valeur <code>0</code> correspond à l'équilibre.</p>
		 * @return	un <code>int</code> commpris entre <code>-100</code> et <code>100</code>
		 */
		public function get pan():int {
			return Math.round(_soundProperties.pan * 100);
		}
		
		[Bindable]
		/**	<p>Définit si les pistes de la liste de lecture sont jouées aléatoirement ou non.</p>
		 * 	<p>Lors d'une lecture aléatoire, les pistes de la liste de lecture sont jouées dans le
		 * 	désordre, et aucune piste n'est répétée tant que toutes les pistes n'ont pas été lues.</p>
		 *
		 * @return	<code>true</code> si la lecture est aléatoire, <code>false</code> sinon
		 */
		public function get random():Boolean {
			return _randomPlayList;
		}
		
		[Bindable]
		/**	<p>Définit si les pistes de la liste de lecture sont jouées en boucle ou non.</p>
		 *
		 * @return	<code>true</code> si la lecture est repetee, <code>false</code> sinon
		 */
		public function get repeat():Boolean {
			return _repeatPlayList;
		}
		
		[Bindable]
		/**	<p>Definit la position de la tete de lecture sur la piste courante. La position est exprimee en milliseconde
		 * 	(1 s = 1000 ms / 1ms = 0,001s)</p>
		 *
		 * @return	un entier representant le temps ecoule depuis le debut de la piste en millisecondes
		 */
		public function get currentTrackPosition():uint {
			if (_soundChannel)
				return _soundChannel.position;
			else
				return 0;
		}
		
		/**	<p>Piste qui est actuellement a l'ecoute. Permet d'acceder aux informations de la piste, telles que
		 * 	le titre, l'auteur ou encore l'album</p>
		 *
		 * 	@return	une instance de <code>PisteAudio</code>
		 */
		public function get currentTrack():Track {
			return _currentTrack;
		}
		
		/**	<p>Tableau contenant le nom des fichiers appartenant à la liste de lecture courante.</p>
		 *
		 *	@return 	un Array de String
		 */
		public function get playList():Array {
			var list:Array = new Array;
			for each (var url:String in _playList)
				list.push(giveFileName(url));
			return list;
		}
		
		/**	<p>Niveau de debug du programme :
		 * 	<ul>
		 * 		<li><b>0</b> : aucune trace n'est affichee</li>
		 * 		<li><b>1</b> : trace des messages d'informations</li>
		 * 		<li><b>2</b> : trace des messages d'informations et des erreurs</li>
		 * 	</ul></p>
		 *
		 */
		public function get debugLevel():int {
			return _debugLevel;
		}
		
		/*	----------------------------------------------------------- */
		/*	---------------			Setters				--------------- */
		/*	----------------------------------------------------------- */
		public function set volume(value:int):void {
			log(1, "---------------\nSetting volume");
			if (value < 0 || value > 100) {
				log(2, "Invalid volume");
				log(2, "Valid volume in [0;100]");
				throw new AudioPlayerError("Invalid volume.");
			}
			_soundProperties.volume = value / 100;
			if (_soundChannel)
				_soundChannel.soundTransform = _soundProperties;
			log(1, "New volume : " + value);
		}
		
		public function set pan(value:int):void {
			log(1, "---------------\nSetting pan");
			if (value < -100 || value > 100) {
				log(2, "Invalid pan");
				log(2, "Valid pan in [-100;100]");
			}
			_soundProperties.pan = value / 100;
			if (_soundChannel)
				_soundChannel.soundTransform = _soundProperties;
			log(1, "New pan : " + value);
		}
		
		public function set random(value:Boolean):void {
			log(1, "---------------\nSetting random");
			_randomPlayList = value;
			log(1, "Random ? : " + _randomPlayList);
		}
		
		public function set repeat(value:Boolean):void {
			log(1, "---------------\nSetting repeat");
			_repeatPlayList = value;
			log(1, "Repeat ? : " + _repeatPlayList);
		}
		
		public function set currentTrackPosition(value:uint):void {
			log(1, "---------------\nSetting track position");
			if (value < 0 || value > _sound.length) {
				log(2, "Invalid track position");
				log(2, "Valid track position in [0;" + _sound.length + "]");
				throw new AudioPlayerError("Invalid track position.");
			}
			log(1, "New track position : " + value);
			log(1, "pause() and play() will be invoked");
			pause();
			_currentTrackPosition = value;
			play();
		}
		
		public function set debugLevel(value:int):void {
			if (value < 0 || value > 2) {
				log(2, "Invalid debug level");
				log(2, "Valid debug level in [0;2]");
				throw new AudioPlayerError("Invalid debug level.");
			}
			_debugLevel = value;
		}
		
		/*	----------------------------------------------------------- */
		/*	---------------			Utils				--------------- */
		/*	----------------------------------------------------------- */
		private function giveFileName(url:String):String {
			var split:Array = url.split('/');
			return unescape(split[split.length - 1]);
		}
		
		/**	<p>Permet de faire la trace de message dans la console, suivant le niveau
		 * 	de debug spécifié par <code>debugLevel</code></p>
		 *
		 */
		protected function log(level:int, message:*):void {
			if (level <= _debugLevel) {
				var flag:String;
				if (level == 1)
					flag = "[INFOS] ";
				else
					flag = "[ERROR] ";
				trace(flag + String(message));
			}
		}
	}
}
